package com.lonicera;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;

import java.io.IOException;
import java.io.InputStream;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;

import lombok.Data;
import lombok.ToString;
import okhttp3.Call;
import okhttp3.Callback;
import okhttp3.Cookie;
import okhttp3.CookieJar;
import okhttp3.Dispatcher;
import okhttp3.FormBody;
import okhttp3.HttpUrl;
import okhttp3.Interceptor;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import org.apache.commons.io.IOUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

/**
 * Hello world!
 */
public class App {

    private static final Logger LOGGER = LogManager.getLogger(App.class);

    public static void main(String[] args) {
        String cookie = loadCookie();
        Map<String, String> cookieMap = mapCookie(cookie);
        OkHttpClient httpClient = newHttpClient(cookieMap);
        scheduleKeepLogin(httpClient);
        Long address = fetchDefaultAddress(httpClient);
        String sbomCode = "2601010506012";
        //String sbomCode = "2601010452802";
        tryConfirmOrder(httpClient, sbomCode, cookieMap.get("CSRF-TOKEN"), address);
    }

    private static void scheduleKeepLogin(OkHttpClient httpClient) {
        Request request = new Request.Builder()
                .get()
                .url("https://openapi.vmall.com/mcp/message/unreadMsgNum?portal=1&lang=zh-CN&country=CN")
                .build();

        Call call = httpClient.newCall(request);

        call.enqueue(new Callback() {
            @Override
            public void onFailure(Call call, IOException e) {
                LOGGER.error("msg read request error", e);
                randomSleep(5);
                scheduleKeepLogin(httpClient);
            }

            @Override
            public void onResponse(Call call, Response response) throws IOException {
                randomSleep(5);
                scheduleKeepLogin(httpClient);
            }
        });
    }

    private static void randomSleep(int maxMinutes) {
        ThreadLocalRandom random = ThreadLocalRandom.current();
        int minutes = random.nextInt(maxMinutes);
        int seconds = random.nextInt(60);
        int waitSeconds = minutes * 60 + seconds;
        try {
            TimeUnit.SECONDS.sleep(waitSeconds);
        } catch (InterruptedException e) {
            LOGGER.warn("not expect wakeup", e);
            randomSleep(maxMinutes);
        }
    }


    private static void tryConfirmOrder(OkHttpClient httpClient, String sbomCode, String csrfToken, Long addressId) {
        CompletableFuture<Boolean> validOrderFuture = new CompletableFuture<>();
        validOrderFuture.complete(true);
        //validateOrder(httpClient, csrfToken, sbomCode, addressId, validOrderFuture);
        asyncWaitStart(httpClient, sbomCode, validOrderFuture);
        scheduleCreateOrder(validOrderFuture, httpClient, sbomCode, csrfToken, addressId);
    }

    private static void asyncWaitStart(OkHttpClient httpClient, String sbomCode, CompletableFuture<Boolean> validOrderFuture) {
        Request request = new Request.Builder()
                .get()
                .url("https://buy.vmall.com/queryRushbuyInfo.json?portal=1&sbomCodes=" + sbomCode)
                .build();
        Call call = httpClient.newCall(request);
        call.enqueue(new Callback() {
            @Override
            public void onFailure(Call call, IOException e) {

            }

            @Override
            public void onResponse(Call call, Response response) throws IOException {
                String bodyContent = response.body().string();
                RushBuyResponse rushBuyResponse = readValue(bodyContent, RushBuyResponse.class);
                if (rushBuyResponse.getSkuRushBuyInfoList().size() > 0) {
                    RushBuyInfo rushBuyInfo = rushBuyResponse.getSkuRushBuyInfoList().get(0);
                    long startTime = rushBuyInfo.getStartTime();
                    long currentTime = rushBuyResponse.getCurrentTime();
                    LocalDateTime currentDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(currentTime), ZoneId.of("+8"));
                    LocalDateTime startDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(startTime), ZoneId.of("+8"));
                    LOGGER.info("currentTime : {}, startTime : {}, wait {} seconds", currentDateTime, startDateTime, TimeUnit.MILLISECONDS.toSeconds(startTime - currentTime));
                    try {
                        TimeUnit.MILLISECONDS.sleep(startTime - currentTime - 1000);
                    } catch (InterruptedException e) {
                        LOGGER.warn("not expect wakeup", e);
                        asyncWaitStart(httpClient, sbomCode, validOrderFuture);
                        return;
                    }
                    validOrderFuture.complete(true);
                }
            }
        });
    }

    @Data
    @JsonIgnoreProperties(ignoreUnknown = true)
    private static class RushBuyResponse {
        private Long currentTime;
        private boolean success;
        private List<RushBuyInfo> skuRushBuyInfoList;
    }

    @Data
    @JsonIgnoreProperties(ignoreUnknown = true)
    private static class RushBuyInfo {
        private Long startTime;
    }

    private static ExecutorService executorService = Executors.newCachedThreadPool();

    private static void scheduleCreateOrder(CompletableFuture<Boolean> validFuture, OkHttpClient httpClient, String sbomCode, String csrfToken, Long addressId) {
        AtomicBoolean orderCreated = new AtomicBoolean();
        for (int i = 0; i < 1000; i++) {
            asyncCreateOrder(validFuture, httpClient, sbomCode, csrfToken, addressId, orderCreated);
        }
    }

    private static void asyncCreateOrder(CompletableFuture<Boolean> validFuture, OkHttpClient httpClient, String sbomCode, String csrfToken, Long addressId, AtomicBoolean orderCreated) {
        executorService.submit(new Runnable() {
            @Override
            public void run() {
                String content = createOrderBody(csrfToken, addressId, sbomCode);
                RequestBody body = FormBody.create(MediaType.get("application/x-www-form-urlencoded; charset=UTF-8"), content);

                String uri = "https://www.vmall.com/order/createcart.json";

                Request request = new Request
                        .Builder()
                        .url(uri)
                        .method("POST", body)
                        .header("CsrfToken", csrfToken)
                        .build();

                Call call = httpClient.newCall(request);

                try {
                    validFuture.get();
                } catch (InterruptedException | ExecutionException e) {
                    throw new IllegalStateException(e);
                }

                call.enqueue(new Callback() {
                    @Override
                    public void onFailure(Call call, IOException e) {

                    }

                    @Override
                    public void onResponse(Call call, Response response) throws IOException {
                        OrderResponse orderResponse = readValue(response.body().string(), OrderResponse.class);
                        if (orderResponse.isSuccess()) {
                            LOGGER.info("create order success {}", orderResponse);
                            orderCreated.set(true);
                        } else {
                            if (orderCreated.get()) {
                                return;
                            }
                            LOGGER.info("create order fail {}", orderResponse);
                            asyncCreateOrder(validFuture, httpClient, sbomCode, csrfToken, addressId, orderCreated);
                        }
                    }
                });

            }
        });
    }

    private static String createOrderBody(String csrfToken, Long addressId, String sbomCode) {
        return "o2oInfo=&carModel=&vSceneActivityCode="
                + "&usingVoucher=false&rushActivityId="
                + "&buttonMode="
                + "&consigneeAddressId=" + addressId
                + "&orderReq=%5B%7B%22itemId%22%3A%22" + sbomCode + "%22%2C%22qty%22%3A1%2C%22itemType%22%3A%22S0%22%2C%22gifts%22%3A%5B%5D%2C%22subOrderItemReqArgs%22%3A%5B%5D%7D%5D"
                + "&couponCodeList="
                + "&deliveryMethod=1"
                + "&couponCode="
                + "&usePointFlag=true"
                + "&usePetalFlag=false"
                + "&interestOrder="
                + "&msgCode="
                + "&challenge="
                + "&geeValidateUUID="
                + "&validate=&secCode=&seqNo=&type=&orderType=0"
                + "&tenantId=&sourcePortal="
                + "&state=0"
                + "&shopCodes=VMALL-HUAWEIDEVICE%7C"
                + "&effectiveListVMALL-HUAWEIDEVICE=61%7C62&uneffectiveListVMALL-HUAWEIDEVICE="
                + "&titleTypeVMALL-HUAWEIDEVICE=61"
                + "&invoiceTitleVMALL-HUAWEIDEVICE=%E4%B8%AA%E4%BA%BA&taxpayerIdentityNumVMALL-HUAWEIDEVICE="
                + "&vatInvoiceJsonVMALL-HUAWEIDEVICE="
                + "&vatInvoiceCompanyVMALL-HUAWEIDEVICE="
                + "&vatInvoiceDeliveryJsonVMALL-HUAWEIDEVICE=&carReqInfo="
                + "&CsrfToken=" + csrfToken + "&appointDelMsg=%7B%22VMALL-HUAWEIDEVICE%22%3A%22%22%7D"
                + "&usingPointNum=1&usingCouponCodes=&lastSelectedCouponCode=";
    }

    private static void validateOrder(OkHttpClient httpClient, String token, String sbomCode, Long addressId, CompletableFuture<Boolean> future) {
        String uri = "https://www.vmall.com/order/validateOrder.json";

        String content = "orderReq=%5B%7B%22itemId%22%3A%22" + sbomCode + "%22%2C%22qty%22%3A1%2C%22itemType%22%3A%22S0%22%2C%22gifts%22%3A%5B%5D%2C%22subOrderItemReqArgs%22%3A%5B%5D%7D%5D&orderType=0&addressId=" + addressId + "&couponCode=&imeiCode=&rushActivityId=&buttonMode=";
        RequestBody body = FormBody.create(MediaType.get("application/x-www-form-urlencoded; charset=UTF-8"), content);

        Request request = new Request
                .Builder()
                .url(uri)
                .method("POST", body)
                .header("CsrfToken", token)
                .build();
        Call call = httpClient.newCall(request);

        call.enqueue(new Callback() {
            @Override
            public void onFailure(Call call, IOException e) {

            }

            @Override
            public void onResponse(Call call, Response response) throws IOException {
                OrderResponse validResponse = readValue(response.body().string(), OrderResponse.class);
                if (validResponse.isSuccess()) {
                    future.complete(true);
                    LOGGER.info("valid response success", validResponse);
                } else {
                    LOGGER.info("valid response not success", validResponse);
                    validateOrder(httpClient, token, sbomCode, addressId, future);
                }
            }
        });
    }

    private static OkHttpClient newHttpClient(Map<String, String> cookie) {
        Dispatcher dispatcher = new Dispatcher();
        dispatcher.setMaxRequestsPerHost(1000);
        dispatcher.setMaxRequests(20000);

        String userAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36";

        Interceptor agentInterceptor = chain -> {
            Request originalRequest = chain.request();
            Request requestWithUserAgent = originalRequest.newBuilder()
                    .header("User-Agent", userAgent)
                    .build();
            return chain.proceed(requestWithUserAgent);
        };

        return new OkHttpClient.Builder()
                .addInterceptor(agentInterceptor)
                .cookieJar(newCookieJar(cookie))
                .dispatcher(dispatcher)
                .build()
                ;
    }

    private static CookieJar newCookieJar(Map<String, String> cookie) {
        return new CookieJar() {
            private List<Cookie> cookies = createCookies(cookie);

            @Override
            public void saveFromResponse(HttpUrl url, List<Cookie> cookies) {
                this.cookies = cookies;
                LOGGER.info("new cookie write", cookies);
            }

            @Override
            public List<Cookie> loadForRequest(HttpUrl url) {
                return cookies;
            }
        };
    }

    private static List<Cookie> createCookies(Map<String, String> cookieMap) {
        List<Cookie> cookieList = Lists.newArrayList();
        for (String key : cookieMap.keySet()) {
            Cookie cookie = new Cookie
                    .Builder()
                    .name(key)
                    .value(cookieMap.get(key))
                    .domain("vmall.com")
                    .build();
            cookieList.add(cookie);
        }
        return cookieList;
    }

    private static Long fetchDefaultAddress(OkHttpClient httpClient) {
        String uri = "https://openapi.vmall.com/mcp/address/getDefaultAddressList?portal=1&lang=zh_CN&country=CN";
        Request request = new Request
                .Builder()
                .url(uri)
                .method("GET", null)
                .build();
        Call call = httpClient.newCall(request);

        Response response = null;
        try {
            response = call.execute();
            AddressResponse address = readValue(response.body().string(), AddressResponse.class);
            LOGGER.info("fetch address : {}", address);
            return address.getAddressInfo().getId();
        } catch (IOException e) {
            throw new IllegalArgumentException(e);
        }
    }

    private static <T> T readValue(String json, Class<T> clazz) {
        try {
            return OBJECT_MAPPER.readValue(json, clazz);
        } catch (IOException e) {
            LOGGER.error("error json : {}", json);
            throw new IllegalArgumentException(e);
        }
    }

    private static ObjectMapper OBJECT_MAPPER = new ObjectMapper();

    @JsonIgnoreProperties(ignoreUnknown = true)
    @Data
    @ToString
    private static class Address {
        private Long id;
        private String address;
    }

    @JsonIgnoreProperties(ignoreUnknown = true)
    @Data
    @ToString
    private static class AddressResponse {
        private Address addressInfo;
    }

    @JsonIgnoreProperties(ignoreUnknown = true)
    @Data
    @ToString
    private static class OrderResponse {
        private boolean success;
    }

    private static Map<String, String> mapCookie(String cookie) {
        String[] pairs = cookie.split(";\\s?");
        Map<String, String> cookieMap = Maps.newHashMap();
        for (String pair : pairs) {
            String[] keyValues = pair.split("=");
            if (keyValues.length == 2) {
                cookieMap.put(keyValues[0], keyValues[1]);
            }
        }
        return cookieMap;
    }

    private static String loadCookie() {
        ClassLoader loader = Thread.currentThread().getContextClassLoader();
        try (InputStream inputStream = loader.getResourceAsStream("hw-cookie.txt")) {
            List<String> lines = IOUtils.readLines(inputStream, "UTF-8");
            String cookie = String.join("", lines);
            return cookie;
        } catch (IOException e) {
            throw new IllegalArgumentException(e);
        }
    }

}
